Android 使用 Virtual Display 实现屏幕录制

录屏是日常手机使用中的常用问题,在 Android 系统下,系统为开发者提供了录屏相关的底层 API,基于这套 API,开发者可以轻松实现录屏功能。在本文中,通过阅读 ScreenRecordHelper 开源项目,梳理如何实现录屏功能,以及背后的相关知识。

录屏功能依赖一个非常重要的系统概念——Android VirtualDisplay。在 Android 系统中,存在显示屏(Display)的概念,手机自带的屏幕是内置屏幕,有的手机支持连显示器,外接显示器为手机的外部屏幕。但是,Android 系统还支持开发者创建虚拟屏幕。顾名思义,这个屏幕并不实际存在,它位于内存中。录屏功能则基于这一特性。

Warning

录屏特性,需要在 Android 5.0(Lollipop)及以上版本中才会支持。Anyway,在 2024 年,这已经不重要了。Android 5.0 以下手机早已成古董。

本文后续内容,以阅读 ScreenRecordHelper 开源项目作为主线,站在开发者角度,按照实现一个录屏 App 的过程讲述。

目标 App

我们将实现如下 App,点击 START 开始录屏,点击 STOP 结束录屏,并保存到文件:

Pasted image 20240201115323.png
注:图片为 ScreenRecordHelper 项目运行后截图


MainActivity

ScreenRecordHelper 是一个库,先将其视为一个黑箱,来到实例 App 的主界面,看从上层是如何使用这个库的。

核心成员:

// 库实例
private var screenRecordHelper: ScreenRecordHelper? = null

// Assets 目录下内置了一段音乐,最终与视频一同合成保存
private val afdd: AssetFileDescriptor by lazy { assets.openFd("test.aac") }

启动录制

在 START 按钮的点击回调中,对 screenRecordHelper 初始化并启动录制。START 和 STOP 的点击回调均在 onCreate 中设置:

btnStart.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        if (screenRecordHelper == null) {
            screenRecordHelper = ScreenRecordHelper(this, object : ScreenRecordHelper.OnVideoRecordListener {
                override fun onBeforeRecord() {}

                override fun onStartRecord() {
                    play()
                }

                override fun onCancelRecord() {
                    releasePlayer()
                }

                override fun onEndRecord() {
                    releasePlayer()
                }

            }, PathUtils.getExternalStoragePath() + "/nanchen")
        }
        screenRecordHelper?.apply {
            if (!isRecording) {
                startRecord()
            }
        }
    } else {
        Toast.makeText(this@MainActivity.applicationContext, "sorry,your phone does not support recording screen", Toast.LENGTH_LONG).show()
    }
}

其中,创建好 ScreenRecordHelper 实例后,调用 ScreenRecordHelper 的 startRecord 开始录制。

创建实例时,ScreenRecordHelper 允许加入一个回调,监听录制生命周期。其中在开始、取消、结束时,都加入了额外操作,主要是用于播放 Assets 目录下的那段音乐文件。

停止录制

看停止录制的回调:

btnStop.setOnClickListener {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        screenRecordHelper?.apply {
            if (isRecording) {
                // 如果选择带参数的 stop 方法,则录制音频无效
				stopRecord(mediaPlayer!!.duration.toLong(), 15 * 1000, afdd)
            }
        }
    }
}

其中,还是调用 ScreenRecordHelper 对应的 stopRecord 方法完成。

权限授予

ScreenRecordHelper 内部会申请权限,在 MainActivity 的 onActivityResult 中进行接收。MainActivity 只管接收,收到后准发到 ScreenRecordHelper 内部:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && data != null) {
        screenRecordHelper?.onActivityResult(requestCode, resultCode, data)
    }
}

ScreenRecordHelper 类

应用层了解完毕后,接下来进入 ScreenRecordHelper 库内部,该库内部只有一个类 ScreenRecordHelper。

核心成员

ScreenRecordHelper 核心成员如下:

// Android 系统服务,用于捕获设备的屏幕内容和/或音频。
private val mediaProjectionManager by lazy { activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager }

// 它用于录制音频和视频
private var mediaRecorder: MediaRecorder? = null

// 它用于从 MediaProjectionManager 获取一个屏幕捕获会话。
private var mediaProjection: MediaProjection? = null

// 虚拟屏幕,用于镜像屏幕内容,用于屏幕捕获
private var virtualDisplay: VirtualDisplay? = null

// 获取屏幕的尺寸和密度信息
private val displayMetrics by lazy { DisplayMetrics() }

// 录制的视频文件
private var saveFile: File? = null

// 表示是否正在录制
var isRecording = false

// 表示是否录制音频
var recordAudio = false

其中,MediaProjection 是 Android 5.0(Lollipop)及以上版本中引入的一个类,它可以捕获设备的屏幕内容和/或音频。这个类的实例不能直接创建,而是通过 MediaProjectionManagergetMediaProjection 方法获取。


startRecord 开始录制

具体实现如下:

fun startRecord() {
    //...
    // 申请存储和麦克风权限
    PermissionUtils.permission(PermissionConstants.STORAGE, PermissionConstants.MICROPHONE)
            .callback(object : PermissionUtils.SimpleCallback {
                override fun onGranted() {
                    // 权限审批通过
                    Log.d(TAG, "start record")

					// 接下来申请视频录制权限
                    mediaProjectionManager?.apply {
                        // 给上层业务的回调通知
                        listener?.onBeforeRecord()
                        val intent = this.createScreenCaptureIntent()
                        if (activity.packageManager.resolveActivity(
		                        intent, 
		                        PackageManager.MATCH_DEFAULT_ONLY) != null) {
                            activity.startActivityForResult(intent, REQUEST_CODE)
                        } else {
                            showToast(R.string.phone_not_support_screen_record)
                        }
                    }

                }
				//...
            })
            .request()
}

startRecord 中分两步获取权限,首先申请存储和录音权限,通过后再次申请录屏权限。其中,录屏权限的 intent 通过 PermissionUtils 开源库的 createScreenCaptureIntent 方法创建。

当录屏权限也审批通过后,来到 onActivityResult,进行实际的录屏操作:

fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (requestCode == REQUEST_CODE) {
        if (resultCode == Activity.RESULT_OK) {
            // 获取一个 `MediaProjection` 对象,这个对象用于捕获屏幕内容。
            mediaProjection = mediaProjectionManager!!.getMediaProjection(resultCode, data)
            // 实测,部分手机上录制视频的时候会有弹窗的出现
            Handler().postDelayed({
                if (initRecorder()) {
                    isRecording = true
                    mediaRecorder?.start()
                    listener?.onStartRecord()
                } else {
                    showToast(R.string.phone_not_support_screen_record)

                }
            }, 150)
        } else {
            showToast(R.string.phone_not_support_screen_record)
        }
    }
}

initRecorder

首先,initRecorder 执行初始化:

private fun initRecorder(): Boolean {
    Log.d(TAG, "initRecorder")
    var result = true
    // 创建录制文件
    val f = File(savePath)
    if (!f.exists()) {
        f.mkdirs()
    }
    // 创建临时文件
    saveFile = File(savePath, "$saveName.tmp")
    saveFile?.apply {
        if (exists()) {
            delete()
        }
    }
    // MediaRecorder 是 Android 提供的一个用于音频和视频录制的类。
    mediaRecorder = MediaRecorder()
    val width = Math.min(displayMetrics.widthPixels, 1080)
    val height = Math.min(displayMetrics.heightPixels, 1920)
    mediaRecorder?.apply {
	    // 设置音频
        if (recordAudio) {
            setAudioSource(MediaRecorder.AudioSource.MIC)
        }
        // 设置视频源为 Surface。这意味着录制的视频将来自一个 Surface,
        // 这个 Surface 可以是任何可以绘制的表面,
        // 例如一个 View 或者一个 VirtualDisplay。
        setVideoSource(MediaRecorder.VideoSource.SURFACE)
        // 设置输出格式为 MPEG_4。这意味着录制的视频将被编码为 MPEG-4 格式。
        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        // 设置视频编码器为 H.264。这意味着录制的视频将被编码为 H.264 格式。
        setVideoEncoder(MediaRecorder.VideoEncoder.H264)
        // 如果 `recordAudio` 为 `true`,则设置音频编码器为 AMR_NB。
        // 这意味着录制的音频将被编码为 AMR_NB 格式。
        if (recordAudio){
            setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
        }
        // 设置输出文件的路径。
        setOutputFile(saveFile!!.absolutePath)
        // 设置视频的宽度和高度。
        setVideoSize(width, height)
        // 设置视频编码的比特率。
        setVideoEncodingBitRate(8388608)
        // 设置视频的帧率。
        setVideoFrameRate(VIDEO_FRAME_RATE)
        try {
            // 调用 MediaRecorder 的 prepare 进行初始化
            prepare()
            // 创建一个虚拟显示,用于捕获屏幕内容。
            // 返回一个 VirtualDisplay 对象
            virtualDisplay = mediaProjection?.createVirtualDisplay(
		            "MainScreen", 
		            width, height, displayMetrics.densityDpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, 
                    // MediaRecorder 的 surface,设置为 `MediaRecorder` 的视频源。
                    surface, null, null)
            Log.d(TAG, "initRecorder 成功")
        } catch (e: Exception) {
            Log.e(TAG, "IllegalStateException preparing MediaRecorder: ${e.message}")
            e.printStackTrace()
            result = false
        }
    }
    return result
}

这段代码是在设置 MediaRecorder 对象的配置,以便于录制音频和视频。下面是每行代码的详细解释:

视频源连接成功之后,调用 mediaRecorder?.start() 开始录制。


createVirtualDisplay 入参常量

在 createVirtualDisplay 看到有传入常量 DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,参见[1]

常量 说明
VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 当没有内容显示时,允许将内容镜像到专用显示器上
VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY 仅显示此屏幕的内容,不镜像显示其他屏幕的内容。
VIRTUAL_DISPLAY_FLAG_PRESENTATION 创建演示文稿的屏幕。
VIRTUAL_DISPLAY_FLAG_PUBLIC 创建公开的屏幕。
VIRTUAL_DISPLAY_FLAG_SECURE 创建一个安全的屏幕

  1. 技术干货 | 录屏采集实现教程 —— Android 端 - 掘金 ↩︎


本文作者:Maeiee

本文链接:Android 使用 Virtual Display 实现屏幕录制

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!